//
//  main.m
//  DNS
//
//  Created by 珲少 on 2021/9/27.
//

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>

// 定义NDS服务器的地址
char *DNSServer = "114.114.114.114";

// DNS报文中查询区域的查询类型
#define A 1
#define CNAME 5

/*
**DNS报文首部
**这里使用了位域
*/
struct DNS_HEADER {
    // 2字节
    unsigned short ID;
    
    // 需要注意，对于结构体中的位域 数据是从低字节开始填充的
    // 1字节
    unsigned char RD :1;
    unsigned char TC :1;
    unsigned char AA :1;
    unsigned char Opcode :4;
    unsigned char QR :1;
    
    // 1字节
    unsigned char RCODE :4;
    unsigned char Z :3;
    unsigned char RA :1;
    
    // 2字节
    unsigned short QCOUNT;
    // 2字节
    unsigned short ANCOUNT;
    // 2字节
    unsigned short NSCOUNT;
    // 2字节
    unsigned short ARCOUNT;
};

/*
**DNS报文中查询问题区域  4个字节
*/
struct QUESTION {
    unsigned short QTYPE;//查询类型
    unsigned short QCLASS;//查询类
};
// 请求部分的结构
typedef struct {
    unsigned char *QNAME;
    struct QUESTION *question;
} QUERY;

/*
**DNS报文中回答区域的常量字段  10个字节
*/
// 需要注意，因为此结构体中有short和int类型，我们需要将其设置为1字节对齐
#pragma pack(1)
struct R_DATA {
    unsigned short TYPE; //表示资源记录的类型
    unsigned short CLASS; //类
    unsigned int TTL; //表示资源记录可以缓存的时间
    unsigned short RDLENGTH; //数据长度
};
#pragma pack()
/*
**DNS报文中回答区域的资源数据字段
*/
struct RES_RECORD {
    unsigned char *NAME;//资源记录包含的域名
    struct R_DATA *resource;//资源数据
    unsigned char *rdata;
};

// DNS解析方法
void DNS(unsigned char*);
// 域名转换方法
int ChangetoDnsNameFormat(unsigned char*, unsigned char*);

/*
**实现DNS查询功能
*/
void DNS(unsigned char *host) {
    
    // UDP目标地址
    struct sockaddr_in dest;
    // DNS请求的数据结构
    struct DNS_HEADER dns = {};

    printf("\n所需解析域名：%s\n", host);
    
    //建立分配UDP套结字
    int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    //IPv4
    dest.sin_family = AF_INET;
    //53号端口 DNS服务器用的是53号端口
    dest.sin_port = htons(53);
    // 设置IP
    dest.sin_addr.s_addr = inet_addr(DNSServer);//DNS服务器IP

    /*设置DNS报文首部*/
    dns.ID = (unsigned short) htons(getpid());//id设为进程标识符
    dns.QR = 0; //查询
    dns.Opcode = 0; //标准查询
    dns.AA = 0; //不授权回答
    dns.TC = 0; //不可截断
    dns.RD = 1; //期望递归
    dns.QCOUNT = htons(1); //1个问题
    // 不需要的字段置为0
    dns.RA = 0;
    dns.Z = 0;
    dns.RCODE = 0;
    dns.ANCOUNT = 0;
    dns.NSCOUNT = 0;
    dns.ARCOUNT = 0;

    // 进行查询的域名处理 先给100个字节大小
    unsigned char *qname = malloc(100);
    // 转换后会将长度返回
    int nameLength = ChangetoDnsNameFormat(qname, host);//修改域名格式
    // 请求结构
    QUERY question = {};
    question.QNAME = qname;
    struct QUESTION qinfo = {};
    qinfo.QTYPE = htons(A); //查询类型为A
    qinfo.QCLASS = htons(1); //查询类为1
    question.question = &qinfo;
    // 定义要发送的UDP数据 先给65536个字节
    unsigned char buf[65536];
    // 复制DNS头部数据到buf
    memcpy(buf, &dns, sizeof(dns));
    // 移动复制的指针
    unsigned char *point = buf + sizeof(dns);
    // 复制请求的域名到buf
    memcpy(point, question.QNAME, nameLength);
    // 移动复制的指针
    point = point + nameLength;
    // 复制要解析的域名到buf
    memcpy(point, question.question, sizeof(*question.question));
    // buf的总长度
    int length = sizeof(dns) + nameLength + sizeof(*question.question);

    //向DNS服务器发送DNS请求报文
    printf("\n\n发送报文中...");
    if (sendto(s, (char*) buf, length, 0, (struct sockaddr*) &dest,sizeof(dest)) < 0)
    {
        perror("发送失败！");
    }
    printf("发送成功！\n");

    // 从DNS服务器接受DNS响应报文
    unsigned char recvBuf[65536];
    int i = sizeof dest;
    printf("接收报文中...\n");
    recvfrom(s, (char*) recvBuf, 65536, 0, (struct sockaddr*) &dest,(socklen_t*) &i);
    if (length < 0) {
        perror("接收失败！");
    }
    printf("接收成功！\n");
    // 将接收到的DNS数据头部解析到结构体
    struct DNS_HEADER recvDNS = *((struct DNS_HEADER *)recvBuf);

    printf("\n\n响应报文包含: ");
    printf("\n %d个问题", ntohs(recvDNS.QCOUNT));
    printf("\n %d个回答", ntohs(recvDNS.ANCOUNT));
    printf("\n %d个授权服务", ntohs(recvDNS.NSCOUNT));
    printf("\n %d个附加记录\n\n", ntohs(recvDNS.ARCOUNT));
    
    // 头部，域名部分和问题的静态部分长度
    size_t headLength = sizeof(struct DNS_HEADER);
    size_t hostLength = strlen((const char*) qname) + 1;
    size_t qusetionLength = sizeof(struct QUESTION);
    
    // 定义指针，将位置移动到报文的Answer部
    unsigned char *reader = &recvBuf[headLength + hostLength + qusetionLength];

    /*
    **解析接收报文
    */
    // 加2个字节，是因为解析的数据中，域名采用的是指针方式，占两个字节(实际情况这里需要判断是否是指针还是真的域名)
    reader = reader + 2;
    // 将Answer部分的静态数据解析到结构体
    struct R_DATA answer = *((struct R_DATA*) (reader));
    printf("回答类型：%x\n", ntohs(answer.TYPE));
    printf("缓存时间：%d秒\n",ntohl(answer.TTL));
    //指向回答问题区域的资源数据字段
    reader = reader + sizeof(struct R_DATA);
    //判断资源类型是否为IPv4地址
    unsigned char *ip = NULL;
    if (ntohs(answer.TYPE) == A) {
        //解析到的IP数据 指针
        ip = (unsigned char*) malloc(ntohs(answer.RDLENGTH)+1);
        for (int j = 0; j < ntohs(answer.RDLENGTH); j++) {
            ip[j] = reader[j];
        }
        ip[ntohs(answer.RDLENGTH)] = '\0';
    }

    //显示查询结果
    if (ip) {
        long *p;
        p = (long*) ip;
        printf("IPv4地址:%s\n", inet_ntoa(*(struct in_addr*)ip));
    }
    return;
}

/*
**从www.baidu.com转换到3www5baidu3com
*/
int ChangetoDnsNameFormat(unsigned char* dns, unsigned char* host) {
    int lock = 0, i, length = 0;
    strcat((char*) host, ".");

    for (i = 0; i < strlen((char*) host); i++) {
        if (host[i] == '.') {
            *dns++ = i - lock;
            length ++;
            for (; lock < i; lock++) {
                *dns++ = host[lock];
                length ++;
            }
            lock++;
        }
    }
    *dns++ = '\0';
    length ++;
    return length;
}

int main(int argc, const char * argv[]) {
    unsigned char hostname[100] = "huishao.cc";
    //由域名获得IPv4地址，A是查询类型
    DNS(hostname);
    return 0;
}



